/*
 * Copyright 2009 Andreas Borglin andreas.borglin@gmail.com
 *
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
 * use this file except in compliance with the License. You may obtain a copy of
 * the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations under
 * the License.
 */

package borge.mobile.lib.pimp;

import java.io.InputStream;
import java.io.OutputStream;
import java.util.Vector;
import javax.microedition.io.Connector;
import javax.microedition.io.file.FileConnection;

/**
 * Extends BaseHandler and provides code
 * for manipulating the real image on file system
 * @author Andreas Borglin
 */
final class RealHandler extends BaseHandler {

    private ProgressListener progressListener;
    private String outputUrl;
    private String outputFormat;
    private Vector operations;
    private String inputFile;
    private double scaleRatio;
    private int overlayCount;
    private int conflicts;
    private int applyCount;
    private String tempFile;
    
    protected RealHandler() {
        super(false,false);
    }
    
    protected void reset(){
        
        applyCount = 0;
        conflicts = 0;
        overlayCount = 0;
        operations = null;
        
    }
    
    private boolean isConflictingOps(int op, int compOp){
        
        int[] conflicts = imageSupport.getImageOperation(op).getConflictingOps();
        for(int i = 0; i < conflicts.length; ++i){
            
            if(compOp == conflicts[i])
                return true;
            
        }
        
        return false;
    }
    
    /**
     * We can optimize the operations by bundling some operations into one
     * This goes for rotate, flip and resize, since we can calculate the 
     * final result for this.
     * @throws java.lang.Exception
     */
    private void optimizeOperations() throws Exception {
        
        int counter = 0;
        PerformedOperation po = null;
        while(true){
            
            if(counter >= operations.size())
                break;
            
            po = (PerformedOperation)operations.elementAt(counter);
            
            // TODO: Create generic method to handle each case..they are alike..
            
            switch(po.operation){
                
                case ROTATE: {
                    
                    int rotValue = po.values[0];
                    PerformedOperation po2 = null;
                    int icount = counter+1;
     
                    while(true){
                        
                        if(icount >= operations.size())
                            break;
                        
                        po2 = (PerformedOperation)operations.elementAt(icount);
                        
                        if(po2.operation == ROTATE){
                            rotValue += po2.values[0];
                            operations.removeElement(po2);
                        }
                        else if(isConflictingOps(po.operation, po2.operation)){
                            po2.conflict = true;
                            conflicts++;
                            break;
                        }
                        else
                            icount++;
                        
                    }
                    
                    // Calculate final rotation value
                    if(rotValue != po.values[0]){
                        int actualValue = rotValue * 90;
                        if(actualValue >= 360)
                            actualValue %= 360;
                            
                        rotValue = actualValue / 90;
                        
                        if(rotValue == 0){
                            operations.removeElement(po);
                            counter = -1; // start over from 0
                            conflicts = 0;
                        }
                        else
                            po.values[0] = rotValue;
                    }
                    
                    break; // loop until we hit end, rotate or flip
                }
                
                case FLIP:{
                    
                    int horizFlips = (po.values[0] == FLIP_HORIZONTALLY ? 1 : 0);
                    int vertFlips = (po.values[0] == FLIP_VERTICALLY ? 1 : 0);
                    PerformedOperation po2 = null;
                    for(int i = counter+1; i < operations.size();){
                        
                        po2 = (PerformedOperation)operations.elementAt(i);
                        
                        if(po2.operation == FLIP){
                            
                            if(po.values[0] == FLIP_HORIZONTALLY){
                                horizFlips++;
                            }
                            else{
                                vertFlips++;
                            }
                            operations.removeElement(po2);
                        }
                        else if(isConflictingOps(po.operation, po2.operation)){
                            // We have to stop...
                            po2.conflict = true;
                            conflicts++;
                            break;
                        }
                        else
                            i++;
                        
                    }

                    // +2 in case its 0
                    horizFlips = ((horizFlips+2) % 2 == 0 ? 0 : FLIP_HORIZONTALLY);
                    vertFlips = ((vertFlips+2) % 2 == 0 ? 0 : FLIP_VERTICALLY);
                    
                    po.values[0] = horizFlips | vertFlips; // FLIP_BOTH
                    if(po.values[0] == 0){
                        operations.removeElement(po);
                        // start over from 0, since this could remove conflicts..
                        counter = -1; // we have a counter++ further down..
                        conflicts = 0;
                    }
                    
                    break;
                }
                // TODO: Optimize resize
                case RESIZE: {
                    
                    PerformedOperation po2 = null;
                    for(int i = counter+1; i < operations.size();){
                        
                        po2 = (PerformedOperation) operations.elementAt(i);

                        if (po2.operation == RESIZE) {

                            // Percent case
                            if(po2.values.length == 1){
                                // Calculate percentage..of..percentage :-)
                                po.values[0] = (int)(po.values[0] * ((double)po2.values[0]/100.0));
                            }
                            // absolute size values
                            else{
                                // Absolute values override any history,
                                // so just set po values to po2 values
                                po.values = po2.values;
                            }
                            
                            operations.removeElement(po2);
                        } 
                        else if (isConflictingOps(po.operation, po2.operation)) {
                            // We have to stop...
                            po2.conflict = true;
                            conflicts++;
                            break;
                        }
                        else
                            i++;
                    }
                    break;
                }
                    
                // Can't bundle these operations, since first round
                // affects outcome of second...
                case CROP: // fall through
                case FRAME:  // fall through
                case BRIGHTNESS:  // fall through
                case CONTRAST: // fall through
                case GAMMA: // fall through
                case EFFECT: // fall through
                case WHITEBALANCE: // fall through
                case OVERLAY: {
                    genericConflictSearch(counter, po);
                    break;
                }
            }
            
         counter++;   
        }
    }
    
    /**
     * Search for possible conflicts in the operations queue
     * for the current operation.
     * @param counter
     * @param po
     */
    private void genericConflictSearch(int counter, PerformedOperation po){
        
        PerformedOperation po2 = null;
        for (int i = counter + 1; i < operations.size(); ++i) {

            po2 = (PerformedOperation) operations.elementAt(i);

            if (isConflictingOps(po.operation, po2.operation)) {
                po2.conflict = true;
                conflicts++;
                break;
            }
        }
    }
  
    private boolean hasConflictingOperations(){
        return conflicts > 0;
    }
    

    private class WriterThread extends Thread{
        
        private int totalSteps = -1;
        private int curStep = -2;
        
        protected WriterThread(){

        }
        
        private void updateStep(String step){
            if(progressListener != null){
                progressListener.stepUpdate(step, curStep, totalSteps);
                if(curStep != -1)
                    curStep++;
            }
        }
        
        public void run(){

            FileConnection out = null;
            try{

                // write output to temp file in case something fails
                // or we must overwrite input file
                int dirEnd = outputUrl.lastIndexOf('/');
                String dir = outputUrl.substring(0, dirEnd + 1);
                tempFile = dir + "TEMP_" + (System.currentTimeMillis()/1000) + ".any";
             
                // open output file
                out = (FileConnection) Connector.open(tempFile, Connector.WRITE);
                out.create();

                OutputStream os = out.openOutputStream();

                updateStep("Optimizing operations queue");
                // See if we can optimize the operations
                optimizeOperations();
                totalSteps = operations.size() + 3;
                curStep = 1;
                updateStep("Checking for conflicts");
                
                
                // If we can not do it all in one run, we need to cache results internally
                if (hasConflictingOperations()) {
     
                    initialize(inputFile, new ImageOutputStream());
                    effectProvider.setOutputFormat(outputFormat);
 
                    applyOperations();
                    byte[] buf = getCurrentImageData();
                    os.write(buf);


                } // No conflicts..can write to file directly!
                else {
                    initialize(inputFile, os);
                    effectProvider.setOutputFormat(outputFormat);
                    applyOperations();
                }
                
                os.flush();
                os.close();

                // Time to create real file
                // Delete if already exists
                FileConnection realOut = (FileConnection)Connector.open(outputUrl, Connector.READ_WRITE);
                if (realOut.exists()) {
                    realOut.delete();
                }
                realOut.close();

                // Close files/streams in provider in case we are
                // overwriting input file
                close();
                String filename = outputUrl.substring(dirEnd+1, outputUrl.length());
                out.rename(filename);
                out.close();

                updateStep("Saving done!");


            if(progressListener != null)
                progressListener.taskFinished(true, null);
            }
            catch(Throwable e){
                System.out.println("WriterThread run: " + e.toString());
                e.printStackTrace();
                try{
                    close();
                    if(out.getName().indexOf("TEMP") != -1)
                        out.delete();
                    out.close();
                } catch(Exception ignore){}

            if(progressListener != null)
                progressListener.taskFinished(false, e.toString());
            }
           
        }
        
         
        private void applyOperations() throws Exception {

            PerformedOperation po = null;
            for (int i = 0; i < operations.size(); ++i) {

                po = (PerformedOperation) operations.elementAt(i);
                switch (po.operation) {
                
                    case ROTATE: {
                        updateStep("Rotating " + (po.values[0]*90) + " degrees");
                        rotate(po.values[0]); 
                        break;
                    }
                    case FLIP:{
                        if(po.values[0] == FLIP_HORIZONTALLY)
                            updateStep("Flipping horizontally");
                        else if(po.values[0] == FLIP_HORIZONTALLY)
                            updateStep("Flipping vertically");
                        else
                            updateStep("Flipping both ways");
                        flip(po.values[0]); 
                        break;
                    }
                    case RESIZE: {
                        updateStep(handleResize(po.values)); 
                        break;
                    }
                    case CROP: {
                        updateStep("Cropping part of image");
                        crop(po.values[0],po.values[1],po.values[2],po.values[3]); 
                        break;
                    }
                    case FRAME: {
                        updateStep("Adding a " + po.values[0] + "x" + po.values[1] + " frame");
                        frame(po.values[0],po.values[1]); 
                        break;
                    }
                    case BRIGHTNESS: {
                        updateStep("Changing brightness " + (po.values[0]-50) + " steps");
                        setBrightness(po.values[0]); 
                        break;
                    }
                    case CONTRAST: {
                        updateStep("Changing contrast " + (po.values[0]-50) + " steps");
                        setContrast(po.values[0]); 
                        break;
                    }
                    case GAMMA: {
                        updateStep("Changing gamma " + (po.values[0]-50) + " steps");
                        setGamma(po.values[0]); 
                        break;
                    }
                    case WHITEBALANCE: {
                        updateStep("Changing whitebalance to: " + 
                                (imageSupport.getImageOperation(WHITEBALANCE).getSupportedModes())[po.values[0]]);
                        setWhiteBalance(po.values[0]); 
                        break;
                    }
                    case EFFECT: {
                        updateStep("Applying effect: " +
                                (imageSupport.getImageOperation(EFFECT).getSupportedModes())[po.values[0]]);
                        applyEffect(po.values[0]); 
                        break;
                    }
                    case OVERLAY: 
                        int x = (int)((double)po.values[0] / scaleRatio);
                        int y = (int)((double)po.values[1] / scaleRatio);
                        updateStep("Adding overlay image at " + x + ";" + y);
                        addOverlayImage(po.sValue,x,y);
                        break;
                    default: throw new ImageEffectException("Unknown operation: " + po.operation);
                
                }

                /*
                if(po.conflict){
                }*/
            }
            
            updateStep("Applying changes");
            applyChanges();
            applyCount++;

        }
    }
    
    protected void addOverlayImage(String file, int x, int y) throws Exception{
        
        FileConnection fc = (FileConnection)Connector.open(file, Connector.READ);
        InputStream is = fc.openInputStream();
        effectProvider.addOverlayImage(is, x, y, overlayCount++);
       
    }
    
    protected void setPreviewValues(double scaleRatio, Vector operations){
        this.operations = operations;
        this.scaleRatio = scaleRatio;
    }
    
  /** Write the current image data to file */
    protected void writeChangesToFile(String inputFile, String outputURL,
            String outputFormat, ProgressListener pl) throws ImageEffectException {

        //
        System.out.println("RealHandler writeChangesToFile");
        System.out.println("inputFile = " + inputFile);
        System.out.println("outputURL = " + outputURL);
        System.out.println("format = " + outputFormat);
        
        validateUrl(inputFile, true);
        validateUrl(outputURL, false);
        // outputFormat has been validated earlier
        
        if(operations.size() == 0){
            throw new ImageEffectException("No operations to perform");
        }
        
        this.progressListener = pl;
        this.outputUrl = outputURL;
        this.outputFormat = outputFormat;
        this.inputFile = inputFile;
        new WriterThread().start();

    }
    
    private void validateUrl(String url, boolean shouldExist) throws ImageEffectException{
        
        try{
            FileConnection fc = (FileConnection)Connector.open(url, Connector.READ);
            
            if(shouldExist && !fc.exists())
                throw new Exception("Input file does not exist");
        }
        catch(Exception e){
            throw new ImageEffectException(e.toString());
        }
        
    }
    
    
    private String handleResize(int[] arr) throws ImageEffectException{
        
        // percent
        if(arr.length == 1){
            
            double scale = (double)arr[0] / 100.0;
            int newW = (int)(getImageWidth() * scale);
            int newH = (int)(getImageHeight() * scale);
            resize(newW, newH);
            return "Resizing to " + newW + "x" + newH;
        }
        
        // width and height
        else{
            resize(arr[0], arr[1]);
            return "Resizing to " + arr[0] + "x" + arr[1];
        }
        
    }
    
    // Special resize for real image
    protected void resize(int newWidth, int newHeight) throws ImageEffectException {
        
        checkState();
        if(newWidth < 0 || newHeight < 0)
            throw new ImageEffectException("Width and height must be greater than 0");
        
        effectProvider.performOperation(RESIZE, newWidth, newHeight, NO_VALUE, NO_VALUE);

    }
    
    protected void postOperation(PerformedOperation po){
        // should never end up here...
    }
}

